其他
编程语言大牛王根:编程的智慧(上)
数学算法俱乐部
日期:2020年04月17日
正文共:6855字0图
预计阅读时间:18分钟
来源:
http://blog.jobbole.com/95763/
反复推敲代码
写优雅的代码
if (...) { if (...) { ... } else { ... } } else if (...) { ... } else { ... } |
写模块化的代码
避免写太长的函数。如果发现函数太大了,就应该把它拆分成几个更小的。通常我写的函数长度都不超过40行。对比一下,一般笔记本电脑屏幕所能容纳的代码行数是50行。我可以一目了然的看见一个40行的函数,而不需要滚屏。只有40行而不是50行的原因是,我的眼球不转的话,最大的视角只看得到40行代码。如果我看代码不转眼球的话,我就能把整片代码完整的映射到我的视觉神经里,这样就算忽然闭上眼睛,我也能看得见这段代码。我发现闭上眼睛的时候,大脑能够更加有效地处理代码,你能想象这段代码可以变成什么其它的形状。40行并不是一个很大的限制,因为函数里面比较复杂的部分,往往早就被我提取出去,做成了更小的函数,然后从原来的函数里面调用。 制造小的工具函数。如果你仔细观察代码,就会发现其实里面有很多的重复。这些常用的代码,不管它有多短,提取出去做成函数,都可能是会有好处的。有些帮助函数也许就只有两行,然而它们却能大大简化主要函数里面的逻辑。有些人不喜欢使用小的函数,因为他们想避免函数调用的开销,结果他们写出几百行之大的函数。这是一种过时的观念。现代的编译器都能自动的把小的函数内联(inline)到调用它的地方,所以根本不产生函数调用,也就不会产生任何多余的开销。 同样的一些人,也爱使用宏(macro)来代替小函数,这也是一种过时的观念。在早期的C语言编译器里,只有宏是静态“内联”的,所以他们使用宏,其实是为了达到内联的目的。然而能否内联,其实并不是宏与函数的根本区别。宏与函数有着巨大的区别(这个我以后再讲),应该尽量避免使用宏。为了内联而使用宏,其实是滥用了宏,这会引起各种各样的麻烦,比如使程序难以理解,难以调试,容易出错等等。 每个函数只做一件简单的事情。有些人喜欢制造一些“通用”的函数,既可以做这个又可以做那个,它的内部依据某些变量和条件,来“选择”这个函数所要做的事情。比如,你也许写出这样的函数: 1 2 3 4 5 6 7 8 9 10 11 12 13 void foo() {
if (getOS().equals("MacOS")) {
a();
} else {
b();
}
c();
if (getOS().equals("MacOS")) {
d();
} else {
e();
}
}
写这个函数的人,根据系统是否为“MacOS”来做不同的事情。你可以看出这个函数里,其实只有 c()
是两种系统共有的,而其它的a()
,b()
,d()
,e()
都属于不同的分支。这种“复用”其实是有害的。如果一个函数可能做两种事情,它们之间共同点少于它们的不同点,那你最好就写两个不同的函数,否则这个函数的逻辑就不会很清晰,容易出现错误。其实,上面这个函数可以改写成两个函数: 1 2 3 4 5 void fooMacOS() {
a();
c();
d();
}
和 1 2 3 4 5 void fooOther() {
b();
c();
e();
}
如果你发现两件事情大部分内容相同,只有少数不同,多半时候你可以把相同的部分提取出去,做成一个辅助函数。比如,如果你有个函数是这样: 1 2 3 4 5 6 7 8 9 10 void foo() {
a();
b()
c();
if (getOS().equals("MacOS")) {
d();
} else {
e();
}
}
其中 a()
,b()
,c()
都是一样的,只有d()
和e()
根据系统有所不同。那么你可以把a()
,b()
,c()
提取出去:1 2 3 4 void preFoo() {
a();
b()
c();
然后制造两个函数: 1 2 3 4 void fooMacOS() {
preFoo();
d();
}
和 1 2 3 4 void fooOther() {
preFoo();
e();
}
这样一来,我们既共享了代码,又做到了每个函数只做一件简单的事情。这样的代码,逻辑就更加清晰。 避免使用全局变量和类成员(class member)来传递信息,尽量使用局部变量和参数。有些人写代码,经常用类成员来传递信息,就像这样: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class A {
String x;
void findX() {
...
x = ...;
}
void foo() {
findX();
...
print(x);
}
}
首先,他使用 findX()
,把一个值写入成员x
。然后,使用x
的值。这样,x
就变成了findX
和print
之间的数据通道。由于x
属于class A
,这样程序就失去了模块化的结构。由于这两个函数依赖于成员x,它们不再有明确的输入和输出,而是依赖全局的数据。findX
和foo
不再能够离开class A
而存在,而且由于类成员还有可能被其他代码改变,代码变得难以理解,难以确保正确性。如果你使用局部变量而不是类成员来传递信息,那么这两个函数就不需要依赖于某一个class,而且更加容易理解,不易出错: 1 2 3 4 5 6 7 8 9 String findX() {
...
x = ...;
return x;
}
void foo() {
int x = findX();
print(x);
}
写可读的代码
使用有意义的函数和变量名字。如果你的函数和变量的名字,能够切实的描述它们的逻辑,那么你就不需要写注释来解释它在干什么。比如: 1 2 // put elephant1 into fridge2
put(elephant1, fridge2);
由于我的函数名 put
,加上两个有意义的变量名elephant1
和fridge2
,已经说明了这是在干什么(把大象放进冰箱),所以上面那句注释完全没有必要。局部变量应该尽量接近使用它的地方。有些人喜欢在函数最开头定义很多局部变量,然后在下面很远的地方使用它,就像这个样子: 1 2 3 4 5 6 7 void foo() {
int index = ...;
...
...
bar(index);
...
}
由于这中间都没有使用过 index
,也没有改变过它所依赖的数据,所以这个变量定义,其实可以挪到接近使用它的地方:1 2 3 4 5 6 7 void foo() {
...
...
int index = ...;
bar(index);
...
}
这样读者看到 bar(index)
,不需要向上看很远就能发现index
是如何算出来的。而且这种短距离,可以加强读者对于这里的“计算顺序”的理解。否则如果index在顶上,读者可能会怀疑,它其实保存了某种会变化的数据,或者它后来又被修改过。如果index放在下面,读者就清楚的知道,index并不是保存了什么可变的值,而且它算出来之后就没变过。如果你看透了局部变量的本质——它们就是电路里的导线,那你就能更好的理解近距离的好处。变量定义离用的地方越近,导线的长度就越短。你不需要摸着一根导线,绕来绕去找很远,就能发现接收它的端口,这样的电路就更容易理解。 局部变量名字应该简短。这貌似跟第一点相冲突,简短的变量名怎么可能有意义呢?注意我这里说的是局部变量,因为它们处于局部,再加上第2点已经把它放到离使用位置尽量近的地方,所以根据上下文你就会容易知道它的意思:比如,你有一个局部变量,表示一个操作是否成功: 1 2 3 4 5 6 boolean successInDeleteFile = deleteFile("foo.txt");
if (successInDeleteFile) {
...
} else {
...
}
这个局部变量 successInDeleteFile
大可不必这么啰嗦。因为它只用过一次,而且用它的地方就在下面一行,所以读者可以轻松发现它是deleteFile
返回的结果。如果你把它改名为success
,其实读者根据一点上下文,也知道它表示”success in deleteFile”。所以你可以把它改成这样:1 2 3 4 5 6 boolean success = deleteFile("foo.txt");
if (success) {
...
} else {
...
}
这样的写法不但没漏掉任何有用的语义信息,而且更加易读。 successInDeleteFile
这种”camelCase“,如果超过了三个单词连在一起,其实是很碍眼的东西,所以如果你能用一个单词表示同样的意义,那当然更好。不要重用局部变量。很多人写代码不喜欢定义新的局部变量,而喜欢“重用”同一个局部变量,通过反复对它们进行赋值,来表示完全不同意思。比如这样写: 1 2 3 4 5 6 7 8 String msg;
if (...) {
msg = "succeed";
log.info(msg);
} else {
msg = "failed";
log.info(msg);
}
虽然这样在逻辑上是没有问题的,然而却不易理解,容易混淆。变量 msg
两次被赋值,表示完全不同的两个值。它们立即被log.info
使用,没有传递到其它地方去。这种赋值的做法,把局部变量的作用域不必要的增大,让人以为它可能在将来改变,也许会在其它地方被使用。更好的做法,其实是定义两个变量:1 2 3 4 5 6 7 if (...) {
String msg = "succeed";
log.info(msg);
} else {
String msg = "failed";
log.info(msg);
}
由于这两个 msg
变量的作用域仅限于它们所处的if语句分支,你可以很清楚的看到这两个msg
被使用的范围,而且知道它们之间没有任何关系。把复杂的逻辑提取出去,做成“帮助函数”。有些人写的函数很长,以至于看不清楚里面的语句在干什么,所以他们误以为需要写注释。如果你仔细观察这些代码,就会发现不清晰的那片代码,往往可以被提取出去,做成一个函数,然后在原来的地方调用。由于函数有一个名字,这样你就可以使用有意义的函数名来代替注释。举一个例子: 1 2 3 4 5 6 7 8 9 10 ...
// put elephant1 into fridge2
openDoor(fridge2);
if (elephant1.alive()) {
...
} else {
...
}
closeDoor(fridge2);
...
如果你把这片代码提出去定义成一个函数: 1 2 3 4 5 6 7 8 9 void put(Elephant elephant, Fridge fridge) {
openDoor(fridge);
if (elephant.alive()) {
...
} else {
...
}
closeDoor(fridge);
}
这样原来的代码就可以改成: 1 2 3 ...
put(elephant1, fridge2);
...
更加清晰,而且注释也没必要了。 把复杂的表达式提取出去,做成中间变量。有些人听说“函数式编程”是个好东西,也不理解它的真正含义,就在代码里大量使用嵌套的函数。像这样: 1 2 Pizza pizza = makePizza(crust(salt(), butter()),
topping(onion(), tomato(), sausage()));
这样的代码一行太长,而且嵌套太多,不容易看清楚。其实训练有素的函数式程序员,都知道中间变量的好处,不会盲目的使用嵌套的函数。他们会把这代码变成这样: 1 2 3 Crust crust = crust(salt(), butter());
Topping topping = topping(onion(), tomato(), sausage());
Pizza pizza = makePizza(crust, topping);
这样写,不但有效地控制了单行代码的长度,而且由于引入的中间变量具有“意义”,步骤清晰,变得很容易理解。 在合理的地方换行。对于绝大部分的程序语言,代码的逻辑是和空白字符无关的,所以你可以在几乎任何地方换行,你也可以不换行。这样的语言设计是个好东西,因为它给了程序员自由控制自己代码格式的能力。然而,它也引起了一些问题,因为很多人不知道如何合理的换行。有些人喜欢利用IDE的自动换行机制,编辑之后用一个热键把整个代码重新格式化一遍,IDE就会把超过行宽限制的代码自动折行。可是这种自动这行,往往没有根据代码的逻辑来进行,不能帮助理解代码。自动换行之后可能产生这样的代码: 1 if (someLongCondition1()
由于 someLongCondition4()
超过了行宽限制,被编辑器自动换到了下面一行。虽然满足了行宽限制,换行的位置却是相当任意的,它并不能帮助人理解这代码的逻辑。这几个boolean表达式,全都用&&
连接,所以它们其实处于平等的地位。为了表达这一点,当需要折行的时候,你应该把每一个表达式都放到新的一行,就像这个样子:1 if (someLongCondition1()
这样每一个条件都对齐,里面的逻辑就很清楚了。再举个例子: 1 2 log.info("failed to find file {} for command {}, with exception {}", file, command,
exception);
这行因为太长,被自动折行成这个样子。 file
,command
和exception
本来是同一类东西,却有两个留在了第一行,最后一个被折到第二行。它就不如手动换行成这个样子:1 2 log.info("failed to find file {} for command {}, with exception {}",
file, command, exception);
把格式字符串单独放在一行,而把它的参数一并放在另外一行,这样逻辑就更加清晰。 为了避免IDE把这些手动调整好的换行弄乱,很多IDE(比如IntelliJ)的自动格式化设定里都有“保留原来的换行符”的设定。如果你发现IDE的换行不符合逻辑,你可以修改这些设定,然后在某些地方保留你自己的手动换行。
expect(foo).to.be.a('string'); expect(foo).to.equal('bar'); expect(foo).to.have.length(3); expect(tea).to.have.property('flavors').with.length(3); |
— THE END —
☞《线性代数应该这样学》学习小结☞谷歌人工智能算法RankBrain运行原理解析☞收藏一波:常用正则表达式公式总结☞施一公:没有高考,就没有一批非常优秀的社会精英从农村走出来☞知乎热搜可以被人为控制吗?如果可以,怎么操作